package eu.darken.octi.sync.module.core

import com.gitee.wsl.exception.asLog
import com.gitee.wsl.flow.DynamicStateFlow
import com.gitee.wsl.flow.replayingShare
import com.gitee.wsl.flow.setupCommonEventHandlers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import timber.log.Timber
import java.time.Instant
import java.util.*

abstract class BaseModuleRepo<T : Any> constructor(
    private val tag: String,
    private val scope: CoroutineScope,
    private val moduleId: ModuleId,
    dispatcherProvider: CoroutineDispatcher=Dispatchers.Default,
    moduleSettings: ModuleSettings,
    private val moduleSync: ModuleSync<T>,
    private val infoSource: ModuleInfoSource<T>,
    private val moduleCache: ModuleCache<T>,
) : ModuleRepo<T> {

    data class State<T>(
        val moduleId: ModuleId,
        val self: ModuleData<T>? = null,
        val isOthersInitialized: Boolean = false,
        val others: Collection<ModuleData<T>> = emptySet(),
    ) : ModuleRepo.State<T> {

        override val all: Collection<ModuleData<T>>
            get() = (self?.let { listOf(it) } ?: emptyList()) + others
    }

    private val _state = DynamicStateFlow(parentScope = scope + dispatcherProvider) {
        State(
            moduleId = moduleId,
            self = moduleCache.get(moduleSync.ourDeviceId),
            others = (moduleCache.cachedDevices() - moduleSync.ourDeviceId).mapNotNull { moduleCache.get(it) },
        )
    }

    override val state: Flow<State<T>> = _state.flow

    private val refreshTrigger = MutableStateFlow(UUID.randomUUID())

    override suspend fun updateSelf(self: ModuleData<T>?) {
       Timber.d( "updateSelf(self=$self)" )
        _state.updateBlocking {
            moduleCache.set(moduleSync.ourDeviceId, self)
            copy(self = self)
        }
    }

    override suspend fun updateOthers(newOthers: Collection<ModuleData<T>>) {
       Timber.d( "updateOthers(newOthers=$newOthers)" )
        _state.updateBlocking {
            // Delete all except ourself, then set with new known data, to remove stale data
            (moduleCache.cachedDevices() - moduleSync.ourDeviceId).forEach { moduleCache.set(it, null) }
            newOthers.forEach { moduleCache.set(it.deviceId, it) }
            copy(others = newOthers)
        }
    }

    override suspend fun refresh() {
       Timber.d( "refresh()" )
        refreshTrigger.value = UUID.randomUUID()
    }

    private val readFlow = moduleSettings.isEnabled
        .flatMapLatest { isEnabled ->
            if (!isEnabled) {
                Timber.w( "$moduleId is disabled" )
                flowOf(emptyList())
            } else {
                moduleSync.others
            }
        }
        .onEach { othersData ->
            updateOthers(othersData)
        }
        .setupCommonEventHandlers(tag) { "readFlow" }

    private val writeFLow = moduleSettings.isEnabled
        .flatMapLatest { isEnabled ->
            if (!isEnabled) {
                Timber.w( "$moduleId is disabled" )
                flowOf(null)
            } else {
                refreshTrigger.flatMapLatest { infoSource.info }
            }
        }
        .distinctUntilChanged()
        .map {
            if (it == null) return@map null
            ModuleData(
                modifiedAt = Instant.now(),
                deviceId = moduleSync.ourDeviceId,
                moduleId = moduleId,
                data = it
            )
        }
        .onEach { selfData ->
            try {
                selfData?.let { moduleSync.sync(selfData) }
            } catch (e: Exception) {
               Timber.e( "Failed to sync data: ${e.asLog()}" )
            }
            updateSelf(selfData)
        }
        .setupCommonEventHandlers(tag) { "writeFLow" }

    override val keepAlive: Flow<Unit> = combine(
        readFlow,
        writeFLow
    ) { _, _ -> }
        .setupCommonEventHandlers(tag) { "keepAlive" }
        .replayingShare(scope + dispatcherProvider)
}